raop: fix read-side deadlock in PatchedIceCastClient (#2849)#2850
Open
dantidote wants to merge 1 commit into
Open
raop: fix read-side deadlock in PatchedIceCastClient (#2849)#2850dantidote wants to merge 1 commit into
dantidote wants to merge 1 commit into
Conversation
When miniaudio.stream_any's decoder init issues a read larger than
(BUFFER_SIZE - position) while position is still inside the seekable
headroom, PatchedIceCastClient.read deadlocks against its downloader
thread:
- read() polls until len(buffer) >= num_bytes, where len(buffer) is
size (post-position bytes).
- The downloader's _buffer.fits(BLOCK_SIZE) guard checks the *raw*
buffer length, not size, so once raw is at BUFFER_SIZE it refuses
to add even though size has room to grow with consumption.
- Reader waits for the buffer to grow; downloader cannot grow it
until the reader consumes. Polling loop hits DEFAULT_TIMEOUT (10s)
and miniaudio surfaces the resulting OperationTimeoutError as
DecodeError('failed to init decoder', -1).
Small streams escape because the HTTP body completes within the 10s
window: _stop_stream flips True and the loop's other exit fires.
Detect 'raw buffer cannot grow' in the polling loop and let
self._buffer.get() return a short read. miniaudio's drmp3 handles
short reads by re-calling; once subsequent reads advance position
past HEADROOM_SIZE the buffer releases its headroom and becomes a
normal sliding window, after which the downloader can refill.
Tests use pytest-httpserver to serve the existing static_3sec.ogg
fixture (small, exercises the fast path) and a new audio_long.mp3
(~80 KiB silence, just over BUFFER_SIZE) to trigger the deadlock.
On master the long test fails after ~10s with DecodeError(-1); with
this fix both tests pass in under a second.
Also bump the miniaudio dev pin from 1.61 to 1.71 in
requirements/requirements.txt. The deadlock fires on 1.71's decoder
init call sequence (the new seek(0, END) plus extra round-trip
leaves position at 2048 when the read(65536) fires); 1.61's init
ends with seek(0, START) which resets position before the same
read, hiding the bug. Real users `pip install pyatv` and resolve
miniaudio>=1.45 to the newest available (1.71 today, and only 1.71
has Python 3.14 wheels), but the old dev pin meant pyatv's own CI
didn't exercise the bug pattern users hit. Bumping aligns CI with
what users actually run so the new regression test serves as a real
guard.
This was referenced Jun 2, 2026
|
You can add "fixes #2849" in the description so it links the issue with the PR and will auto-close it when the PR got merged 😉 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fixes #2849
When miniaudio.stream_any's decoder init issues a read larger than (BUFFER_SIZE - position) while position is still inside the seekable headroom, PatchedIceCastClient.read deadlocks against its downloader thread:
Small streams escape because the HTTP body completes within the 10s window: _stop_stream flips True and the loop's other exit fires.
Detect 'raw buffer cannot grow' in the polling loop and let self._buffer.get() return a short read. miniaudio's drmp3 handles short reads by re-calling; once subsequent reads advance position past HEADROOM_SIZE the buffer releases its headroom and becomes a normal sliding window, after which the downloader can refill.
Tests use pytest-httpserver to serve the existing static_3sec.ogg fixture (small, exercises the fast path) and a new audio_long.mp3 (~80 KiB silence, just over BUFFER_SIZE) to trigger the deadlock. On master the long test fails after ~10s with DecodeError(-1); with this fix both tests pass in under a second.
Also bump the miniaudio dev pin from 1.61 to 1.71 in requirements/requirements.txt. The deadlock fires on 1.71's decoder init call sequence (the new seek(0, END) plus extra round-trip leaves position at 2048 when the read(65536) fires); 1.61's init ends with seek(0, START) which resets position before the same read, hiding the bug. Real users
pip install pyatvand resolve miniaudio>=1.45 to the newest available (1.71 today, and only 1.71 has Python 3.14 wheels), but the old dev pin meant pyatv's own CI didn't exercise the bug pattern users hit. Bumping aligns CI with what users actually run so the new regression test serves as a real guard.